-- Copyright (c) 2024 Huawei Technologies Co., Ltd.
-- openUBMC is licensed under Mulan PSL v2.
-- You can use this software according to the terms and conditions of the Mulan PSL v2.
-- You may obtain a copy of Mulan PSL v2 at:
--         http://license.coscl.org.cn/MulanPSL2
-- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
-- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
-- MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
-- See the Mulan PSL v2 for more details.

local singleton = require 'mc.singleton'
local patition_object = require 'os_patition.patition'
local log = require 'mc.logging'
local skynet = require 'skynet'
local class = require 'mc.class'
local org_freedesktop_dbus = require 'sd_bus.org_freedesktop_dbus'
local match_rule = org_freedesktop_dbus.MatchRule
local skynet_queue = require 'skynet.queue'
local json = require 'cjson'
local mdb = require 'mc.mdb'
local add_event = require 'add_event'
local client = require 'storage.client'
local PatitionMgmt = class()

local HOST_AGENT_APP = 'bmc.kepler.host_agent'
local PARITION_PATH = '/bmc/kepler/Systems/1/Sms/1/ComputerSystem/Systems/1/Summary'
local PARITION_INTERFACE = 'bmc.kepler.sms.redfish'
local HOST_AGENT_OPEN = 0
local HOST_AGENT_CLOSE = 1
local SYSTEMID = 1

function PatitionMgmt:ctor(bus, reset_db, obj_mgmt)
    self.bus = bus
    self.reset_db = reset_db
    self.patition_collection = {}
    self.queue = skynet_queue()
    self.obj_mgmt = obj_mgmt
    self.percentage_threshold = -1  -- 默认无效值
end

function PatitionMgmt:init()
    -- 1、注册信号：（1）监听bma状态 （2）监听分区属性
    -- 2、获取分区信息
    skynet.fork_once(function()
        self:monitor()
        self:load_patitions()
    end)
    -- config对象可能先注册
    if self.obj_mgmt.c_storageconfig.obj then
        self.percentage_threshold = self.obj_mgmt.c_storageconfig.obj.DiskPartitionUsagePercentageThreshold
    end
    self.obj_mgmt.c_storageconfig.percentage_update_signal:on(function(value)
        self.percentage_threshold = value
        self:check_percentage_event()
    end)
end

function PatitionMgmt:get_patition_len()
    local len = 0
    for _, _ in pairs(self.patition_collection) do
        len = len + 1
    end
    return len
end

function PatitionMgmt:reset_patitions()
    for k, v in pairs(self.patition_collection) do
        self:check_percentage_event(k, true) -- 卸载磁盘对象时需要清除告警
        v:del()
        self.patition_collection[k] = nil
    end
    self.patition_collection = {}
end

function PatitionMgmt:renew_patitions(patitions)
    self:reset_patitions()
    local index = 1
    for _, patition in pairs(patitions) do
        local obj = patition_object.new(self.obj_mgmt:CreateDiskPartition(SYSTEMID, index), patition)
        self.patition_collection[patition.OSDriveName] = obj
        index = index + 1
        -- 查询是否有未恢复的告警
        local alarm_params = self:query_alarm_db(patition.OSDriveName)
        if alarm_params then
            obj.alarm_flag = true
            obj.alarm_usage = alarm_params[2]
            obj.alarm_threshold = alarm_params[3]
        end
    end
    -- 注册完时全量检测一次告警
    self:check_percentage_event()
end

function PatitionMgmt:update_patitions(patitions)
    for _, patition in pairs(patitions) do
        local drive_name = patition.OSDriveName
        if self.patition_collection[drive_name] then
            self.patition_collection[drive_name]:update(patition)
            -- 更新数据后检测告警条件
            self:check_percentage_event(drive_name)
        end
    end
end

function PatitionMgmt:is_need_renew(patitions)
    local patitions_len = 0
    for _, patition in pairs(patitions) do
        if not self.patition_collection[patition.OSDriveName] then
            return true
        end
        patitions_len = patitions_len + 1
    end
    if patitions_len ~= self:get_patition_len() then
        return true
    end
    return false
end

function PatitionMgmt:parse_and_update(storage)
    for _ = 1, 3 do
        local ok, err = pcall(self.parse_and_update_patition, self, storage)
        if ok then
            log:info('[storage]update patition success')
            return
        end
        log:error('[storage]update patition fail, err%s', err)
        skynet.sleep(100)
    end
end

function PatitionMgmt:parse_and_update_patition(storage)
    local info = storage:value()
    local patitions_tbl = {}
    for _, v in pairs(info) do
        table.insert(patitions_tbl, json.decode(v:value()))
    end
    if self:is_need_renew(patitions_tbl) then
        log:info('[storage]renew patition')
        self:renew_patitions(patitions_tbl)
    else
        log:info('[storage]update patition')
        self:update_patitions(patitions_tbl)
    end
end

function PatitionMgmt:monitor()
    self:monitor_patitions()
    self:monitor_bma_status()
end

function PatitionMgmt:monitor_patitions()
    local status_sig = match_rule.signal('PropertiesChanged'):with_path(PARITION_PATH)
    self.patition_slot = self.bus:match(status_sig, function(msg)
        local interface, props = msg:read()
        if interface ~= PARITION_INTERFACE or not props.Storage then
            return
        end

       log:info('BMA patitions change to %s', props.Storage:value())
        self.queue(function()
            self:parse_and_update(props.Storage:value())
        end)
    end)

    local add = org_freedesktop_dbus.ObjMgrInterfacesAdded
    local add_sig = match_rule.signal(add.name):with_interface(add.interface):with_path_namespace(
        PARITION_PATH)
    self.add_slot = self.bus:match(add_sig, function(msg)
        self.queue(function()
            self:load_patitions()
            log:notice('[storage]sms interface add, load patitions success')
        end)
    end)

    local del = org_freedesktop_dbus.ObjMgrInterfacesRemoved
    local del_sig = match_rule.signal(del.name):with_interface(del.interface):with_path_namespace(
        PARITION_PATH)
    self.del_slot = self.bus:match(del_sig, function(msg)
        self.queue(function()
            self:reset_patitions()
            log:notice('[storage]sms interface del, reset patitions success')
        end)
    end)
end

function PatitionMgmt:monitor_bma_status()
    client:OnSmsStatusPropertiesChanged(function(values, _, _)
        local state = values.State:value()
        log:info('BMA status change to %s', state)
        if state == HOST_AGENT_CLOSE then
            self.queue(function()
                self:reset_patitions()
                log:notice('[storage]reset patitions success')
            end)
        elseif state == HOST_AGENT_OPEN then
            self.queue(function()
                self:load_patitions()
                log:notice('[storage]reload patitions success')
            end)
        end
    end)
end

function PatitionMgmt:get_sms_storage()
    for _ = 1, 3 do
        local ok, sms_obj = pcall(mdb.get_object, self.bus, PARITION_PATH, PARITION_INTERFACE)
        if ok then
            return sms_obj
        end
        log:error('[storage]get sms storage fail, err:%s', sms_obj)
        skynet.sleep(100)
    end
end

function PatitionMgmt:load_patitions()
    for _ = 1, 3 do
        local bus_resource = self.bus
        local err, storage_info = bus_resource:pcall(HOST_AGENT_APP, PARITION_PATH,
            'org.freedesktop.DBus.Properties', 'Get', 'ss', PARITION_INTERFACE, 'Storage')
        if not err and storage_info then
            self.queue(function()
                self:parse_and_update(storage_info:value())
            end)
            break
        end
        skynet.sleep(100)
    end
end

function PatitionMgmt:check_percentage_event(drive_name, force_deassert)
    if self.percentage_threshold == -1 then -- 阈值未同步
        return
    end
    -- 传入名称时检测单个硬盘，否则全量检测
    local patitions = self.patition_collection
    if drive_name and self.patition_collection[drive_name] then
        patitions = {}
        patitions[drive_name] = self.patition_collection[drive_name]
    end
    for name, drive in pairs(patitions) do
        if not drive.obj then
            goto continue
        end
        local is_over = false
        if not force_deassert then -- 强制恢复
            is_over = drive.obj.Usage >= self.percentage_threshold
        end
        if is_over ~= drive.alarm_flag then
            local usage = is_over and drive.obj.Usage or drive.alarm_usage
            local threshold = is_over and self.percentage_threshold or drive.alarm_threshold
            local ok = add_event.get_instance().generate_percentage_threshold(is_over, name, usage, threshold)
            if ok then
                drive.alarm_flag = not drive.alarm_flag
                drive.alarm_usage = usage
                drive.alarm_threshold = threshold
                self:update_alarm_db(is_over, name, usage, threshold)
            end
        end
        ::continue::
    end
end

function PatitionMgmt:update_alarm_db(is_insert, drive_name, usage, threshold)
    local t = self.reset_db.tables['t_storage_reset']
    local record = t({PerId = drive_name, Key = 'alarm_props'})
    if not is_insert then
        record:delete()
        return
    end
    local params = json.encode({drive_name, usage, threshold})
    if next(record.__datas) then -- 脏数据更新
        record.Value = params
    else
        record = t({PerId = drive_name, Key = 'alarm_props', Value = params})
    end
    record:save()
end

function PatitionMgmt:query_alarm_db(drive_name)
    if not drive_name then
        return
    end
    local t = self.reset_db.tables['t_storage_reset']
    local record = t({PerId = drive_name, Key = 'alarm_props'})
    if next(record.__datas) then
        return json.decode(record.__datas.Value)
    end
end

return singleton(PatitionMgmt)